﻿using Nx.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;

namespace Nx.Services
{
    public class HostService : IHostService
    {
        private Dictionary<Tuple<Uri, Type>, ServiceHost> _typedServiceHosts;
        private Dictionary<Tuple<Uri, object>, ServiceHost> _singletonServiceHosts;
        private ILogger Logger;

        public event EventHandler ServiceHostFaulted;

        private void OnServiceHostOpened(object sender, EventArgs e)
        {
            Logger.Info("Service host opened");
        }

        private void OnServiceHostClosed(object sender, EventArgs e)
        {
            Logger.Info("Service host closed");
        }

        private void OnServiceHostFaulted(object sender, EventArgs e)
        {
            Logger.Error("Service host faulted");

            if (this.ServiceHostFaulted != null)
            {
                ServiceHostFaulted(sender, e);
            }
        }

        private void OnServiceHostException(Exception ex)
        {
            Logger.Error("Service host exception : {0}", ex.Message);
            if (ex.InnerException != null)
            {
                Logger.Error("InnerException : {0}", ex.InnerException.Message);
            }
        }

        private ServiceHost CreateServiceHost<TService>(Uri baseAddress)
        {
            ServiceHost host = new ServiceHost(typeof(TService), baseAddress);
            return InitHost(host);
        }

        private ServiceHost CreateServiceHost<TService>(TService singletonInstance, Uri baseAddress)
        {
            ServiceHost host = new ServiceHost(singletonInstance, baseAddress);
            return InitHost(host);
        }

        public void AddService<TService>(ServiceEndpoint endpoint, Uri baseAddress)
        {
            if (!TypedServiceHostsContain<TService>(baseAddress))
            {
                try
                {
                    ServiceHost host = CreateServiceHost<TService>(baseAddress);
                    host.AddServiceEndpoint(endpoint);
                    host.Open();

                    _typedServiceHosts.Add(new Tuple<Uri, Type>(baseAddress, typeof(TService)), host);
                }
                catch (Exception ex)
                {
                    OnServiceHostException(ex);
                }
            }
        }

        public void AddService<TContract, TService>(Binding binding, Uri baseAddress)
            where TService : TContract
        {
            if (!TypedServiceHostsContain<TService>(baseAddress))
            {
                try
                {
                    ServiceHost host = CreateServiceHost<TService>(baseAddress);

                    host.AddServiceEndpoint(typeof(TContract), binding, baseAddress);

                    host.Open();

                    _typedServiceHosts.Add(new Tuple<Uri, Type>(baseAddress, typeof(TService)), host);
                }
                catch (Exception ex)
                {
                    OnServiceHostException(ex);
                }
            }
        }

        public void AddService<TService>(IEnumerable<ServiceEndpoint> endpoints, Uri baseAddress)
        {
            if (!TypedServiceHostsContain<TService>(baseAddress))
            {
                try
                {
                    ServiceHost host = CreateServiceHost<TService>(baseAddress);

                    foreach (ServiceEndpoint endpoint in endpoints)
                    {
                        if (endpoint.Contract.ContractType == typeof(IMetadataExchange) && !(endpoint.Binding.Name.Equals("MetadataExchangeTcpBinding")))
                        {
                            EnableMex(host);
                        }
                        else if (endpoint.Contract.ContractType == typeof(IMetadataExchange) && (endpoint.Binding.Name.Equals("MetadataExchangeTcpBinding")))
                        {
                            EnableTcpMex(host);
                        }

                        host.AddServiceEndpoint(endpoint);
                    }

                    host.Open();

                    _typedServiceHosts.Add(new Tuple<Uri, Type>(baseAddress, typeof(TService)), host);
                }
                catch (Exception ex)
                {
                    OnServiceHostException(ex);
                }
            }
            else
            {
                Logger.Error("Typed service hosts : already contain this key : {0}", typeof(TService).Name);
            }
        }

        public void AddService<TService>(TService singletonInstance, ServiceEndpoint endpoint, Uri baseAddress)
        {
            if (!SingletonServiceHostsContain<TService>(singletonInstance))
            {
                try
                {
                    ServiceHost host = CreateServiceHost<TService>(singletonInstance, baseAddress);

                    host.AddServiceEndpoint(endpoint);

                    host.Open();

                    _singletonServiceHosts.Add(new Tuple<Uri, object>(baseAddress, singletonInstance), host);
                }
                catch (Exception ex)
                {
                    OnServiceHostException(ex);
                }
            }
        }

        public void AddService<TService>(TService singletonInstance, IEnumerable<ServiceEndpoint> endpoints, Uri baseAddress)
        {
            if (!SingletonServiceHostsContain<TService>(singletonInstance))
            {
                try
                {
                    ServiceHost host = CreateServiceHost<TService>(singletonInstance, baseAddress);

                    foreach (ServiceEndpoint endpoint in endpoints)
                    {
                        if (endpoint.Contract.ContractType == typeof(IMetadataExchange) && !(endpoint.Binding.Name.Equals("MetadataExchangeTcpBinding")))
                        {
                            EnableMex(host);
                        }
                        else if (endpoint.Contract.ContractType == typeof(IMetadataExchange) && (endpoint.Binding.Name.Equals("MetadataExchangeTcpBinding")))
                        {
                            EnableTcpMex(host);
                        }

                        host.AddServiceEndpoint(endpoint);
                    }

                    host.Open();

                    _singletonServiceHosts.Add(new Tuple<Uri, object>(baseAddress, singletonInstance), host);
                }
                catch (Exception ex)
                {
                    OnServiceHostException(ex);
                }
            }
        }

        public void RemoveService<TService>(Uri baseAddress)
        {
            try
            {
                if (TypedServiceHostsContain<TService>(baseAddress))
                {
                    var key = GetTypedServiceHostKey<TService>(baseAddress);
                    ServiceHost host = _typedServiceHosts[key];

                    host.Close();

                    _typedServiceHosts.Remove(key);
                }
                else if (SingletonServiceHostsContain<TService>(baseAddress))
                {
                    var key = GetSingletonServiceHostKey<TService>(baseAddress);
                    ServiceHost host = _singletonServiceHosts[key];

                    host.Close();

                    _singletonServiceHosts.Remove(key);
                }
            }
            catch (Exception ex)
            {
                OnServiceHostException(ex);
            }
        }

        public void RemoveService<TService>(TService singletonInstance)
        {
            if (SingletonServiceHostsContain<TService>(singletonInstance))
            {
                try
                {
                    var key = GetSingletonServiceHostKey<TService>(singletonInstance);
                    ServiceHost host = _singletonServiceHosts[key];
                    host.Close();

                    IDisposable instance = key.Item2 as IDisposable;
                    if (instance != null)
                    {
                        instance.Dispose();
                    }

                    _singletonServiceHosts.Remove(key);
                }
                catch (Exception ex)
                {
                    OnServiceHostException(ex);
                }
            }
        }

        public CommunicationState GetServiceState<TService>(Uri baseAddress)
        {
            if (TypedServiceHostsContain<TService>(baseAddress))
            {
                var key = GetTypedServiceHostKey<TService>(baseAddress);
                return _typedServiceHosts[key].State;
            }
            else if (SingletonServiceHostsContain<TService>(baseAddress))
            {
                var key = GetSingletonServiceHostKey<TService>(baseAddress);
                return _singletonServiceHosts[key].State;
            }
            else
            {
                throw new InvalidOperationException(string.Format("Service of type {0} is not registered.", typeof(TService)));
            }
        }

        public CommunicationState GetServiceState<TService>(TService singletonInstance)
        {
            if (SingletonServiceHostsContain<TService>(singletonInstance))
            {
                var key = GetSingletonServiceHostKey<TService>(singletonInstance);
                return _singletonServiceHosts[key].State;
            }
            else
            {
                throw new ArgumentException(string.Format("Service {0} is not registered.", singletonInstance), "singletonService");
            }
        }

        public HostService(ILogFactory logFactory)
        {
            Logger = logFactory.CreateLogger("HostService");
            _typedServiceHosts = new Dictionary<Tuple<Uri, Type>, ServiceHost>();
            _singletonServiceHosts = new Dictionary<Tuple<Uri, object>, ServiceHost>();
        }

        ~HostService()
        {
            Dispose(false);
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        private void Dispose(bool disposing)
        {
            if (disposing)
            {
                // close normal typed service hosts
                foreach (var keyValuePair in _typedServiceHosts)
                {
                    var host = keyValuePair.Value;
                    ReleaseHost(host);
                }

                // close singleton service hosts
                // if service instances are IDisposable - dispose thems
                foreach (var keyValuePair in _singletonServiceHosts)
                {
                    var host = keyValuePair.Value;
                    var instance = keyValuePair.Key.Item2 as IDisposable;

                    if (instance != null)
                    {
                        instance.Dispose();
                        instance = null;
                    }

                    ReleaseHost(host);
                }

                _typedServiceHosts.Clear();
                _singletonServiceHosts.Clear();

                if (Logger != null)
                {
                    Logger.Dispose();
                    Logger = null;
                }
            }
        }

        #region Auxiliaries
        private void EnableMex(ServiceHost host)
        {
            ServiceMetadataBehavior smb = new ServiceMetadataBehavior();
            smb.HttpGetEnabled = true;
            host.Description.Behaviors.Add(smb);
        }

        private void EnableTcpMex(ServiceHost host)
        {
            ServiceMetadataBehavior smb = new ServiceMetadataBehavior();
            host.Description.Behaviors.Add(smb);
        }

        private ServiceHost InitHost(ServiceHost host)
        {
            host.Opened += OnServiceHostOpened;
            host.Faulted += OnServiceHostFaulted;
            host.Closed += OnServiceHostClosed;

            return host;
        }

        private void ReleaseHost(ServiceHost host)
        {
            host.Close();

            host.Opened -= OnServiceHostOpened;
            host.Faulted -= OnServiceHostFaulted;
            host.Closed -= OnServiceHostClosed;

            host = null;
        }

        private Tuple<Uri, Type> GetTypedServiceHostKey<TService>(Uri baseAddress)
        {
            return _typedServiceHosts.Single(k => k.Key.Item1.Equals(baseAddress) && k.Key.Item2.Equals(typeof(TService))).Key;
        }

        private Tuple<Uri, object> GetSingletonServiceHostKey<TService>(Uri baseAddress)
        {
            return _singletonServiceHosts.Single(k => k.Key.Item1.Equals(baseAddress)).Key;
        }

        private Tuple<Uri, object> GetSingletonServiceHostKey<TService>(TService singletonInstance)
        {
            return _singletonServiceHosts.Single(k => k.Key.Item2.GetHashCode().Equals(singletonInstance.GetHashCode())).Key;
        }

        private bool TypedServiceHostsContain<TService>(Uri baseAddress)
        {
            return _typedServiceHosts.Keys
                .Any(k => k.Item1.Equals(baseAddress) && k.Item2.Equals(typeof(TService)));
        }

        private bool SingletonServiceHostsContain<TService>(Uri baseAddress)
        {
            return _singletonServiceHosts.Keys.Any(k => k.Item1.Equals(baseAddress));
        }

        private bool SingletonServiceHostsContain<TService>(TService singletonInstance)
        {
            return _singletonServiceHosts.Keys.Any(k => k.Item2.GetHashCode().Equals(singletonInstance.GetHashCode()));
        }
        #endregion
    }
}
